/*
 *  Copyright 2011 Alexey Andreev.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package org.xthl.core;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.nop.core.BeanScope;
import org.nop.core.ManagedBean;
import org.nop.sql.DataManager;
import org.nop.sql.DataResult;
import org.nop.sql.QueryBuilder;
import org.nop.util.Injected;
import org.xthl.core.api.CreateUserCommand;
import org.xthl.core.api.IllegalActivationHashException;
import org.xthl.core.api.RestrictedAccessException;
import org.xthl.core.api.Role;
import org.xthl.core.api.User;
import org.xthl.core.api.UserDetails;
import org.xthl.core.api.UserLogic;
import org.xthl.core.data.DefaultRoleSource;
import org.xthl.core.data.PermissionSource;
import org.xthl.core.data.RoleSource;
import org.xthl.core.data.UserPermissionSource;
import org.xthl.core.data.UserRoleSource;
import org.xthl.core.data.UserSource;

/**
 * 
 * @author Alexey Andreev
 */
@ManagedBean(iface = UserLogic.class, scope = BeanScope.REQUEST)
public class UserLogicImpl implements UserLogic {
    private DataManager dataManager;
    private QueryBuilder qb;
    private UsersConfigService config;
    private Integer currentUserId;
    private Random random = new Random();
    private Set<Integer> existingUsers;
    private UserDetails currentUser;
    private boolean currentUserLoaded;
    private PermissionChecker permissionChecker;

    @Injected
    public UserLogicImpl(DataManager dataManager, UserSession session, UsersConfigService config,
            PermissionChecker permissionChecker) {
        this.dataManager = dataManager;
        this.qb = dataManager.getQueryBuilder();
        if (session != null) {
            currentUserId = session.getUserId();
        }
        this.config = config;
        this.permissionChecker = permissionChecker;
    }

    @Override
    public boolean checkPermission(String name) {
        return permissionChecker.checkPermission(currentUserId,
                permissionChecker.getPermissionId(name));
    }

    @Override
    public void requirePermission(String name) throws RestrictedAccessException {
        if (!checkPermission(name)) {
            throw new RestrictedAccessException(name);
        }
    }

    @Override
    public UserDetails getCurrentUser() {
        if (!currentUserLoaded) {
            currentUserLoaded = true;
            currentUser = currentUserId != null ? getUser(currentUserId) : null;
        }
        return currentUser != null ? currentUser.clone() : null;
    }

    @Override
    public UserDetails getUser(int id) {
        UserSource user = qb.get(UserSource.class);
        DataResult result = dataManager.exec(qb.with(user)
                .filter(user.id().eq(id))
                .fetch(user.id(), user.login(), user.creationDate(), user.activated(),
                        user.enabled()));
        return getUserDetails(result);
    }

    private UserDetails getUserDetails(DataResult result) {
        if (!result.next()) {
            return null;
        }
        UserDetails dto = new UserDetails();
        dto.setId(result.getInt(1));
        dto.setLogin(result.getString(2));
        dto.setCreationDate(result.getDate(3));
        dto.setActivated(result.getBoolean(4));
        dto.setEnabled(result.getBoolean(5));

        UserRoleSource userRole = qb.get(UserRoleSource.class);
        RoleSource role = qb.get(RoleSource.class);
        result = dataManager.exec(qb.with(userRole)
                .join(role, role.id().eq(userRole.roleId()))
                .filter(userRole.userId().eq(dto.getId()))
                .fetch(role.id(), role.name(), role.system()));
        List<Role> roleDtos = new ArrayList<Role>();
        while (result.next()) {
            Role roleDto = new Role();
            roleDto.setId(result.getInt(1));
            roleDto.setName(result.getString(2));
            roleDto.setSystem(result.getBoolean(3));
            roleDtos.add(roleDto);
        }
        dto.setRoles(roleDtos);
        return dto;
    }

    private User getUserBrief(DataResult result) {
        User dto = new User();
        dto.setId(result.getInt(1));
        dto.setLogin(result.getString(2));
        dto.setCreationDate(result.getDate(3));
        dto.setActivated(result.getBoolean(4));
        dto.setEnabled(result.getBoolean(5));
        return dto;
    }

    @Override
    public UserDetails getUserByLogin(String login) {
        UserSource user = qb.get(UserSource.class);
        DataResult result = dataManager.exec(qb.with(user)
                .filter(user.login().eq(login))
                .fetch(user.id(), user.login(), user.creationDate(), user.activated(),
                        user.enabled()));
        return getUserDetails(result);
    }

    @Override
    public List<User> getLastUsers(int limit) throws RestrictedAccessException {
        UserSource user = qb.get(UserSource.class);
        DataResult result = dataManager.exec(qb.with(user)
                .sortDesc(user.creationDate())
                .take(limit)
                .fetch(user.id(), user.login(), user.creationDate(), user.activated(),
                        user.enabled()));
        List<User> dtos = new ArrayList<User>();
        while (result.next()) {
            dtos.add(getUserBrief(result));
        }
        return dtos;
    }

    @Override
    public List<User> findUsers(String loginPart, int limit) throws RestrictedAccessException {
        UserSource user = qb.get(UserSource.class);
        DataResult result = dataManager.exec(qb.with(user)
                .filter(user.login().lower().like("%" + loginPart.toLowerCase() + "%"))
                .sortDesc(user.creationDate())
                .take(limit)
                .fetch(user.id(), user.login(), user.creationDate(), user.activated(),
                        user.enabled()));
        List<User> dtos = new ArrayList<User>();
        while (result.next()) {
            dtos.add(getUserBrief(result));
        }
        return dtos;
    }
    
    public boolean isSpecialUser(int userId) {
        UserRoleSource userRole = qb.get(UserRoleSource.class);
        RoleSource role = qb.get(RoleSource.class);
        return dataManager.exec(qb.with(userRole)
                .join(role, role.id().eq(userRole.roleId()))
                .filter(userRole.userId().eq(userId))
                .filter(role.system().isTrue())
                .fetch(role.id())).next();
    }
    
    public boolean anySpecialUser(Set<Integer> userIds) {
        UserRoleSource userRole = qb.get(UserRoleSource.class);
        RoleSource role = qb.get(RoleSource.class);
        return dataManager.exec(qb.with(userRole)
                .join(role, role.id().eq(userRole.roleId()))
                .filter(userRole.userId().inNumbers(userIds))
                .filter(role.system().isTrue())
                .fetch(role.id())).next();
    }

    public void requireModifyUser(int userId, String permission) {
        if (currentUserId == null || currentUserId.intValue() != userId) {
            if (isSpecialUser(userId)) {
                requirePermission("user.manage");
            } else {
                requirePermission(permission);
            }
        }
    }
    
    public void requireModifyUsers(Set<Integer> userIds, String permission) {
        if (currentUserId != null && userIds.contains(currentUserId)) {
            requireModifyUser(currentUserId, permission);
            userIds = new HashSet<Integer>(userIds);
            userIds.remove(currentUserId);
        }
        if (anySpecialUser(userIds)) {
            requirePermission("user.manage");
        } else {
            requirePermission(permission);
        }
    }

    @Override
    public String getUserEmail(int id) throws RestrictedAccessException {
        if (currentUserId == null || currentUserId.intValue() != id) {
            requirePermission("user.edit");
        }
        UserSource user = qb.get(UserSource.class);
        DataResult result = dataManager.exec(qb.with(user)
                .filter(user.id().eq(id))
                .fetch(user.email()));
        if (!result.next()) {
            return null;
        }
        return result.getString(1);
    }

    @Override
    public void setUserEmail(int id, String email) throws RestrictedAccessException {
        checkUserExists(id);
        requireModifyUser(id, "user.edit");
        UserSource user = qb.get(UserSource.class);
        dataManager.exec(qb.with(user)
                .filter(user.id().eq(id))
                .set(user.email(), email));
    }
    
    private void checkUsersExist(Set<Integer> userIds) {
        UserSource user = qb.get(UserSource.class);
        DataResult result = dataManager.exec(qb.with(user)
                .filter(user.id().inNumbers(userIds))
                .fetch(qb.rowCount()));
        result.next();
        if (result.getInt(1) < userIds.size()) {
            throw new IllegalArgumentException("Some users do not exist");
        }
    }

    @Override
    public void enableUsers(Set<Integer> userIds, boolean enabled)
            throws RestrictedAccessException {
        checkUsersExist(userIds);
        UserSource user = qb.get(UserSource.class);
        requireModifyUsers(userIds, "user.enable");
        dataManager.exec(qb.with(user)
                .filter(user.id().inNumbers(userIds))
                .set(user.enabled(), enabled));
    }

    @Override
    public void setUserPermissions(int userId, Set<String> permissions)
            throws RestrictedAccessException {
        checkUserExists(userId);
        UserPermissionSource userPermission = qb.get(UserPermissionSource.class);
        dataManager.exec(qb.with(userPermission)
                .filter(userPermission.userId().eq(userId))
                .delete());
        for (String permission : permissions) {
            dataManager.exec(qb.insertInto(userPermission)
                    .field(userPermission.userId(), userId)
                    .field(userPermission.permissionId(),
                            permissionChecker.getPermissionId(permission)));
        }
    }

    @Override
    public void setUserRoles(int userId, Set<Integer> roleIds) throws RestrictedAccessException {
        UserRoleSource userRole = qb.get(UserRoleSource.class);
        checkUserExists(userId);
        requireModifyUser(userId, "user.edit");
        dataManager.exec(qb.with(userRole)
                .filter(userRole.userId().eq(userId))
                .delete());
        for (int roleId : roleIds) {
            dataManager.exec(qb.insertInto(userRole)
                    .field(userRole.userId(), userId)
                    .field(userRole.roleId(), roleId));
        }
    }

    @Override
    public UserDetails createUser(CreateUserCommand command) throws RestrictedAccessException {
        UserSource user = qb.get(UserSource.class);
        boolean exists = dataManager.exec(qb.with(user)
                .filter(user.login().eq(command.getLogin()))
                .fetch(user.id())).next(); 
        if (exists) {
            throw new IllegalArgumentException("User already exists " + command.getLogin());
        }
        Date now = new Date();
        for (int i = 0; i < 20; ++i) {
            int id = random.nextInt(1000000000);
            String activationHash = config.getActivationMode() == UserActivationMode.SELF ?
                    User.getPasswordHash(id + "." + System.currentTimeMillis() +
                    user.login()) : null;
            boolean activated = config.getActivationMode() == UserActivationMode.AUTO;
            boolean ok = dataManager.exec(qb.insertInto(user)
                    .field(user.id(), id)
                    .field(user.creationDate(), now)
                    .field(user.login(), command.getLogin())
                    .field(user.passwordHash(), command.getPassword() != null ? 
                            User.getPasswordHash(command.getPassword()) : null)
                    .field(user.email(), command.getEmail())
                    .field(user.activationHash(), activationHash)
                    .field(user.activated(), activated));
            if (ok) {
                UserRoleSource userRole = qb.get(UserRoleSource.class);
                DefaultRoleSource defRole = qb.get(DefaultRoleSource.class);
                dataManager.exec(qb.with(defRole)
                        .filter(defRole.type().eq("default"))
                        .insertInto(userRole)
                        .field(userRole.roleId(), defRole.roleId())
                        .field(userRole.userId(), qb.wrap(id)));
                UserDetails details = new UserDetails();
                details.setEnabled(true);
                details.setActivated(false);
                details.setCreationDate(now);
                details.setId(id);
                details.setLogin(command.getLogin());
                List<Role> roleDtos = new ArrayList<Role>();
                RoleSource role = qb.get(RoleSource.class);
                DataResult result = dataManager.exec(qb.with(defRole)
                        .join(role, role.id().eq(defRole.roleId()))
                        .filter(defRole.type().eq("default"))
                        .fetch(role.id(), role.name(), role.system()));
                while (result.next()) {
                    Role roleDto = new Role();
                    roleDto.setId(result.getInt(1));
                    roleDto.setName(result.getString(2));
                    roleDto.setSystem(result.getBoolean(3));
                    roleDtos.add(roleDto);
                }
                details.setRoles(roleDtos);
                return details;
            }
        }
        throw new RuntimeException("Could not generate user id");
    }
    
    private void checkUserExists(int id) {
        if (existingUsers == null) {
            existingUsers = new HashSet<Integer>();
        }
        if (existingUsers.contains(id)) {
            return;
        }
        UserSource user = qb.get(UserSource.class);
        boolean exists = dataManager.exec(qb.with(user)
                .filter(user.id().eq(id))
                .fetch(user.activationHash())).next();
        if (!exists) {
            throw new IllegalArgumentException("User not found: " + id);
        }
        existingUsers.add(id);
    }

    @Override
    public void activateUsers(Set<Integer> userIds) throws RestrictedAccessException {
        checkUsersExist(userIds);
        UserSource user = qb.get(UserSource.class);
        dataManager.exec(qb.with(user)
                .filter(user.id().inNumbers(userIds))
                .set(user.activated(), true)
                .set(user.activationHash(), (String)null));
    }

    @Override
    public void activateWithHash(int id, String hash) throws IllegalActivationHashException {
        UserSource user = qb.get(UserSource.class);
        DataResult result = dataManager.exec(qb.with(user)
                .filter(user.id().eq(id))
                .fetch(user.activationHash(), user.activated()));
        if (!result.next()) {
            throw new IllegalArgumentException("User does not exist: " + id);
        }
        if (!hash.equals(result.getString(1))) {
            throw new IllegalActivationHashException();
        }
        dataManager.exec(qb.with(user)
                .filter(user.id().eq(id))
                .set(user.activated(), true)
                .set(user.activationHash(), (String)null));
    }

    @Override
    public List<String> getUserPermissions(int userId) throws RestrictedAccessException {
        UserPermissionSource userPerm = qb.get(UserPermissionSource.class);
        PermissionSource perm = qb.get(PermissionSource.class);
        DataResult result = dataManager.exec(qb.with(userPerm)
                .join(perm, perm.id().eq(userPerm.permissionId()))
                .filter(userPerm.userId().eq(userId))
                .fetch(perm.name()));
        List<String> permissions = new ArrayList<String>();
        while (result.next()) {
            permissions.add(result.getString(1));
        }
        return permissions;
    }

    @Override
    public void setUserPassword(int userId, String password) throws RestrictedAccessException {
        checkUserExists(userId);
        requireModifyUser(userId, "user.edit");
        UserSource user = qb.get(UserSource.class);
        dataManager.exec(qb.with(user)
                .filter(user.id().eq(userId))
                .set(user.passwordHash(), User.getPasswordHash(password)));
    }

    @Override
    public boolean checkPassword(int userId, String password) throws RestrictedAccessException {
        if (currentUserId == null) {
            throw new RestrictedAccessException("");
        }
        if (currentUserId != userId) {
            requirePermission("user.edit");
        }
        UserSource user = qb.get(UserSource.class);
        DataResult result = dataManager.exec(qb.with(user)
                .filter(user.id().eq(userId))
                .fetch(user.passwordHash()));
        if (!result.next()) {
            throw new IllegalArgumentException("User not found: " + userId);
        }
        return User.getPasswordHash(password).equals(result.getString(1));
    }
}
